Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mdoc-support #2054

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

auer-martin
Copy link
Contributor

No description provided.

Copy link

changeset-bot bot commented Oct 8, 2024

🦋 Changeset detected

Latest commit: 95eb0db

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@credo-ts/core Patch
@credo-ts/action-menu Patch
@credo-ts/anoncreds Patch
@credo-ts/askar Patch
@credo-ts/bbs-signatures Patch
@credo-ts/cheqd Patch
@credo-ts/drpc Patch
@credo-ts/indy-sdk-to-askar-migration Patch
@credo-ts/indy-vdr Patch
@credo-ts/node Patch
@credo-ts/openid4vc Patch
@credo-ts/question-answer Patch
@credo-ts/react-native Patch
@credo-ts/tenants Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Signed-off-by: Martin Auer <[email protected]>
Signed-off-by: Martin Auer <[email protected]>
Comment on lines 117 to 119
responseUri:
authorizationRequest.authorizationRequestPayload.response_uri ??
authorizationRequest.authorizationRequestPayload.response_uri,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is redundant

@@ -109,6 +110,14 @@ export class OpenId4VcSiopHolderService {
challenge: nonce,
domain: clientId,
presentationSubmissionLocation: DifPresentationExchangeSubmissionLocation.EXTERNAL,
openid4vp: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how are these values mapped to the presentation? Ideally we don't add oid4vp specific properties to the pex service, but i can understand if it's not possible otherwise

Copy link
Contributor Author

@auer-martin auer-martin Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also don't like it, but I don't see an easy better way to propagate the values. There are used to calculate the session transcript

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay and this is openid4vp specific, or is it mdoc specific?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I guess depends on the viewpoint the data we need is expected to come for openid4vp, but we need it to create mdoc presentations.

@@ -109,6 +110,14 @@ export class OpenId4VcSiopHolderService {
challenge: nonce,
domain: clientId,
presentationSubmissionLocation: DifPresentationExchangeSubmissionLocation.EXTERNAL,
openid4vp: {
clientId,
verifierGeneratedNonce: nonce,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be extracted from the challenge property in the pex service itself. As it's now redundant, and can lead to inconsistencies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this intentionally. I don't think we should use a challenge property. openid4vp is very specific. To me it would be completely unclear why the verification of a device response will fail after changing a e.g. domain/challenge property which have no relation at all to Mdoc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think it's quite common to use one nonce and apply that in all places that need a nonce. That way you keep consistency. we use the presentation exchange nonce in the w3c vp signature creation, so i don't see why we can't use it in the mdoc presentation creation.

I rather reuse the existing property, as there's really no reason to use a different nonce for the mdoc presentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because the nonce for a w3c jwt vp / sd-jwt-vc is also based on the challenge. We just call it challenge in our api, but all VPs use this as the nonce/challenge.

openid4vp: {
clientId,
verifierGeneratedNonce: nonce,
mdocGeneratedNonce: await agentContext.wallet.generateNonce(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to make sure to reuse this nonce when encrypting the resposne (we generate a new nonce there at the moment, but these should be the same)

@@ -41,6 +45,8 @@ export function getSphereonVerifiablePresentation(
return JsonTransformer.toJSON(verifiablePresentation) as SphereonW3cVerifiablePresentation
} else if (verifiablePresentation instanceof W3cJwtVerifiablePresentation) {
return verifiablePresentation.serializedJwt
} else if (verifiablePresentation instanceof MdocVerifiablePresentation) {
throw new CredoError('Mdoc verifiable credential is not yet supported.')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new CredoError('Mdoc verifiable credential is not yet supported.')
throw new CredoError('Mdoc verifiable presentation is not yet supported.')


const { deviceResponseBase64Url, presentationSubmission } = await MdocDeviceResponse.openId4Vp(agentContext, {
mdocs: [Mdoc.fromBase64Url(mdocRecord.base64Url)],
presentationDefinition: presentationDefinition as DifPresentationExchangeDefinitionV2,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if v1 is used?

claimFormat: presentationToCreate.claimFormat,
})

continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of continue we could use an else clause maybe? I had a hard time understanding this code, until i spotted this continue statement

Comment on lines 34 to 35
.filter((c) => c instanceof MdocRecord === false)
.map((c) => getSphereonOriginalVerifiableCredential(c as SdJwtVcRecord | W3cCredentialRecord))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.filter((c) => c instanceof MdocRecord === false)
.map((c) => getSphereonOriginalVerifiableCredential(c as SdJwtVcRecord | W3cCredentialRecord))
.filter((c): c is Exclude<typeof c, MdocRecord> => c instanceof MdocRecord === false)
.map((c) => getSphereonOriginalVerifiableCredential(c))

...selectResultsRaw,
areRequiredCredentialsPresent:
mdocPresentationDefinition.input_descriptors.length >= 1
? 'warn' // we don't know yet wheater the required credentials are present
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when will we know this? (i don't see warn changed to error / info?)

Comment on lines +53 to +59
public get deviceSignedNamespaces(): MdocNameSpaces {
if (this.issuerSignedDocument instanceof DeviceSignedDocument === false) {
throw new MdocError(`Cannot get 'device-namespaces from a IssuerSignedDocument. Must be a DeviceSignedDocument.`)
}

return this.issuerSignedDocument.allDeviceSignedNamespaces
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't device signed the presentation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. It can also include device-signed namespaces. Essentially self-attested claims.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but shouldn't this be on the mdoc presentation then?

document.addIssuerNameSpace(namespace, namespaceRecord)
}

const cert = X509Certificate.fromEncodedCertificate(issuerCertificate)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see we're mostly using string certificates here. Do we want to use that as the primary method to pass around certificate in Credo (i haven't made my mind up yet, just thinking 🤔 ).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind. We essentially also set trustedCerts as strings. That's why I have chosen this approach

export interface X509ModuleConfigOptions {
  /**
   *
   * Array of trusted base64-encoded certificate strings in the DER-format.
   */
  trustedCertificates?: [string, ...string[]]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string is fine 👍

}

const cert = X509Certificate.fromEncodedCertificate(issuerCertificate)
const issuerPrivateJwk = await getJwkFromKey(options.issuerKey ?? cert.publicKey)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we check if the issuerKey and cert key match? Can't we always just extract the key from the cert?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think this should work. Not sure why it doesn't. It may be related to the issue I mentioned below with the jwk representation of keys.

const issuerPrivateJwk = await getJwkFromKey(options.issuerKey ?? cert.publicKey)
const issuerSignedDocument = await document.sign(
{
issuerPrivateKey: issuerPrivateJwk.toJson(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the public key jwk, not the private key jwk right? I think the name is confusing. It's important we don't have to pass the private key, so we can support HSM

const issuerSignedDocument = await document.sign(
{
issuerPrivateKey: issuerPrivateJwk.toJson(),
alg: issuerPrivateJwk.supportedSignatureAlgorithms[0] as 'ES256' | 'ES384' | 'ES512' | 'EdDSA',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the casting here, it can lead to bugs in the future. We should check if index 0 exists, and then also whether it's a supported alg by mdoc (or does it throw if the provided alg is not supported?)

return new Mdoc(issuerSignedDocument)
}

public async verify(agentContext: AgentContext, options?: MdocVerifyOptions): Promise<boolean> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just as an FYI trusted certificaes will need to be aligned with #2052 once merged

getMdocContext(agentContext)
)

await verifier.verifyData({ mdoc: this.issuerSignedDocument }, mdocContext)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it return a verification object or something? Would be good to return more than true/false, but also something we can add later

* @param options {MdocCreateOptions}
* @returns {Promise<Mdoc>}
*/
public async create(options: MdocCreateOptions) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does create also sign? I think in SD JWT API we use sign so might be nice to stay consistent?

Comment on lines 56 to 58
const { mac0, jwk, options } = input
const { data, signature, alg } = mac0.getRawVerificationData(options)
return await verifyWithJwk({ jwk, signature, data, alg })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this still use the credo wallet to verify the signature? I can't see the context or something passed around. It is important.

Aslo it's prob ok for now, but we should try to stay away from doing crypto operations outside of the wallet. Also with e.g. the p256 shared secret calcuation etc.. The more we can do in the wallet, the better.

So if you can provide a list of all cryptographic operations that are needed, we can look to move them to the wallet over time

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We just need the key derivation.

},
getPublicKey: async (input) => {
const certificate = new x509.X509Certificate(input.certificate)
const key = await importX509({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do these do? and why are they part of the protokol library?

Copy link
Contributor Author

@auer-martin auer-martin Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please take a look at my comment. There is an issue with how we represent the key as jwk. It probably should be compressed. Nothing from the protokoll library should remain here. Atleast related to crypto.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which comment? I can't find it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    //expected
    //{
    //kty: 'EC',
    //x: 'OFBq4YMKg4w5fTifsytwBuJf_7E7VhRPXiNm52S3q1E',
    //y: 'EyIAXV8gyt5FcRsYHhz4ryz97rjL0uogxHO6jMZr3bg',
    //crv: 'P-256'
    //}

    //actual
    //{
    //kty: 'EC',
    //crv: 'P-256',
    //x: 'OFBq4YMKg4w5fTifsytwBuJf_7E7VhRPXiNm52S3q1ETIgBdXyDK3kVxGxgeHPiv',
    //y: 'LP3uuMvS6iDEc7qMxmvduNeBp_oWscK1x-3_1KKYDayIctdDcpXHi8HcbehAfVIK'
    //}

    //const comp = X509Certificate.fromRawCertificate(input.certificate)
    //const x = getJwkFromKey(comp.publicKey).toJson()
    //// eslint-disable-next-line @typescript-eslint/no-unused-vars
    //const { use, ...jwk } = x
    //return jwk


const publicDeviceJwk = COSEKey.import(deviceKeyInfo.deviceKey).toJWK()

deviceKeyInfo.deviceKey
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
deviceKeyInfo.deviceKey

return {
deviceResponseBase64Url: TypedArrayEncoder.toBase64URL(deviceResponseMdoc.encode()),
presentationSubmission: MdocDeviceResponse.createPresentationSubmission({
id: 'MdocPresentationSubmission ' + agentContext.wallet.generateNonce(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can also just use an uuid?

Suggested change
id: 'MdocPresentationSubmission ' + agentContext.wallet.generateNonce(),
id: 'MdocPresentationSubmission ' + await agentContext.wallet.generateNonce(),

Comment on lines +165 to +169
const trustedCerts =
options?.trustedCertificates ?? agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates

if (!trustedCerts) {
throw new MdocError('No trusted certificates found. Cannot verify mdoc.')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same with integraiont of pr #2052

@@ -22,6 +22,7 @@ export interface X509CreateSelfSignedCertificateOptions {
notBefore?: Date
notAfter?: Date
name?: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think name can just be a string with CN=X, N=X I think that might be nicer to keep it like that. Otherwise we'll have to add a custom prop for everything

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or we need to nest / rename it

this.base64Url = TypedArrayEncoder.toBase64URL(cborEncode(issuerSigned))
}

public static _interalFromIssuerSignedDocument(issuerSignedDocument: IssuerSignedDocument) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think if it's public we shouldn't call it _internal, but just fromIssuerSignedDocument. Or we don't allow creating it from IssuerSignedDocument at all (but something else).

Comment on lines +31 to +34
/**
* The base64Url-encoded device response string.
*/
deviceResponse: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to add both the mdoc and the deviceResponse base64? The encoded device response also contains the mdoc right?

If it's for internal APIs it's ok, but for public APIs I'd like to avoid options that could conflict (what if they don't match?)

deviceResponse: string

/**
* The private part of the ephemeral key used in the session where the DeviceResponse was obtained. This is only required if the DeviceResponse is using the MAC method for device authentication.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the private part. It's a public key that can be used during verificadtion to know which private key to use.

Suggested change
* The private part of the ephemeral key used in the session where the DeviceResponse was obtained. This is only required if the DeviceResponse is using the MAC method for device authentication.
* The public ephemeral key used in the session where the DeviceResponse was obtained. This is only required if the DeviceResponse is using the MAC method for device authentication.

Also, isn't the public key added in the device respnose, or do we need to make sure it's MAC'd to this specific key?

* The trusted base64-encoded issuer certificate string in the DER-format.
*/
issuerCertificate: string
holderPublicKey: Key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either both key, or both PublicKey, but issuerKey and holderPublicKey doesn't make sense I think

Suggested change
holderPublicKey: Key
holderKey: Key

}

public async createDeviceResponse(agentContext: AgentContext, options: MdocDeviceResponseOpenId4VpOptions) {
return MdocDeviceResponse.openId4Vp(agentContext, options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is device response an openid4vp specific object, if we're using BLE don't we use device response? If so I don't think the underlying API should be called openId4Vp?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or should this method be called createOpenId4VpDeviceResponse?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice but usually we name tests mdoc-deviceResponse-openid4vc.test.ts instead of using dots (which are more for extension types)


// this is the ISSUER side
{
mdoc = await Mdoc.create(agent.context, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we also want to add an oid4vci flow with mdoc? OR lateR?

Comment on lines +230 to +267
;[
[
'clientId',
{
clientId: 'wrong',
responseUri,
verifierGeneratedNonce,
mdocGeneratedNonce,
},
] as const,
[
'responseUri',
{
clientId,
responseUri: 'wrong',
verifierGeneratedNonce,
mdocGeneratedNonce,
},
] as const,
[
'verifierGeneratedNonce',
{
clientId,
responseUri,
verifierGeneratedNonce: 'wrong',
mdocGeneratedNonce,
},
] as const,
[
'mdocGeneratedNonce',
{
clientId,
responseUri,
verifierGeneratedNonce,
mdocGeneratedNonce: 'wrong',
},
] as const,
].forEach(([name, values]) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might look a bit clear?

Suggested change
;[
[
'clientId',
{
clientId: 'wrong',
responseUri,
verifierGeneratedNonce,
mdocGeneratedNonce,
},
] as const,
[
'responseUri',
{
clientId,
responseUri: 'wrong',
verifierGeneratedNonce,
mdocGeneratedNonce,
},
] as const,
[
'verifierGeneratedNonce',
{
clientId,
responseUri,
verifierGeneratedNonce: 'wrong',
mdocGeneratedNonce,
},
] as const,
[
'mdocGeneratedNonce',
{
clientId,
responseUri,
verifierGeneratedNonce,
mdocGeneratedNonce: 'wrong',
},
] as const,
].forEach(([name, values]) => {
Object.entries({
clientId: {
clientId: 'wrong',
responseUri,
verifierGeneratedNonce,
mdocGeneratedNonce,
}
}).forEach(([name, values]) => {

Copy link
Contributor

@TimoGlastra TimoGlastra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants